로깅이란
로깅은 개발하는 동안 값을 찍어보기 위한 도구가 아니다.
통합 QA와 프로덕션에 배포된 서비스가 실제로 어떻게 동작하는지, 예상하지 못한 문제가 없는지 검증하기 위해 남기는 흔적이자 힌트다.
남기는 목적에 따라 크게 세 가지로 나눌 수 있다.
| 종류 | 무엇을 검증하는가 | 대표 로그 |
|---|---|---|
| 동작 검증 로깅 | 서비스가 의도한 대로 동작하는가 | API 실패, 예외, 백그라운드 작업 시작/종료 |
| 비즈니스 메트릭 로깅 | 계획한 비즈니스와 유저 플로우가 의도대로 동작하는가 | 페이지 노출, 유저 이벤트 |
| 퍼포먼스 로깅 | 서비스가 충분히 빠르고 부드럽게 동작하는가 | TTI, Jank Frame, CPU/메모리 |
동작 검증 로깅
서비스가 의도한 대로 동작하는지 확인하기 위한 로깅이다.
같은 목적이라도 QA 환경과 프로덕션 환경에서 남겨야 하는 양이 다르다.
QA
백엔드와 모바일이 엮여서 통합 QA를 하면 의도하지 않은 버그가 나온다.
이를 빠르게 해결하려면 QA 빌드에서 남겨야 할 로그를 충실히 남겨야 한다.
로그가 부족하면 재현부터 다시 해야 하지만, 로그가 남아 있으면 어느 지점에서 어긋났는지 바로 좁힐 수 있다.
QA 빌드에서는 e, w, i, d, v를 모두 남겨도 된다.
프로덕션
개발과 QA 과정에서는 원인을 직접 확인하고 협업으로 문제를 수정할 수 있다.
하지만 유저가 실제로 사용하는 프로덕션에서는 그런 수정이 불가능하다.
이슈가 Crash 에러만 있는 것도 아니다.
그래서 프로덕션에서는 다음을 알 수 있어야 문제를 해결할 수 있다.
- 어떤 유저가
- 어떤 플로우를 거쳤는지
- 이슈가 나는 플로우에서 페이지의 관련된 상태값들이 어땠는지
단, 프로덕션에서 많은 로그를 남기면 인프라 비용과 유저의 네트워크 리소스가 소모된다.
꼭 필수적인 힌트에 대해서만 로그를 남겨야 하고, 레벨을 낮췄더라도 같은 로그가 연속적으로 불필요하게 발생하는 케이스는 없어야 한다.
두 환경의 차이
| 구분 | QA | 프로덕션 |
|---|---|---|
| 목적 | 통합 과정에서 나온 버그의 원인 좁히기 | 재현할 수 없는 이슈의 원인 추론 |
| 원인 파악 | 직접 확인하고 협업으로 수정 가능 | 남긴 로그로만 추론 가능 |
| 남기는 레벨 | e, w, i, d, v 모두 | e, w, i만 |
| 로그 양 | 충실하게 많이 | 필수적인 힌트만 |
| 제약 | 거의 없음 | 인프라 비용, 유저 네트워크 리소스 |
비즈니스 메트릭 로깅
우리가 계획한 비즈니스나 유저 플로우가 의도대로 동작하는지를 검증하기 위한 흔적이다.
페이지 노출 로깅
어떤 페이지로 진입할 때, 유저가 그 페이지로 진입하게 유도한 UI 요소의 데이터 상태 등을 스냅샷을 찍듯이 남긴다.
유저가 어떤 요인에 이끌려 핵심 기능으로 한 Depth 더 들어왔는지를 살펴보고, 이를 지속적으로 개선하기 위한 로깅이다.
유저 이벤트 로깅
어떤 페이지에 있는 중요한 UI 요소(버튼, 리스트 아이템 등)를 클릭하거나 동작시킬 때 발생하는 로깅이다.
이때 해당 액션을 유도한 UI 요소의 데이터 상태도 함께 남겨야 한다.
클릭했다는 사실만으로는 왜 클릭했는지를 알 수 없기 때문이다.
퍼포먼스 로깅
서비스가 충분히 빠르고 부드럽게 동작하는지를 검증하기 위한 흔적이다.
느리다는 체감은 주관적이므로 숫자로 남겨야 개선 여부를 판단할 수 있다.
TTI(Time to Interactive) 로깅
어떤 페이지로 진입할 때, 유저가 의미 있는 인터랙션을 할 수 있기까지 페이지가 로딩되는 데 걸리는 시간을 단계별로 남긴다.
Jank Frame 로깅
앱이 동작하면서 UI 렌더링이 블락되거나 끊기는 상황을 “Jank Frame이 존재한다”라고 한다.
일정 이상의 Jank Frame이 발생할 때, 어떤 페이지에서 얼마나 발생하는지를 남긴다.
Jank Frame 로깅과 동작 검증 로깅을 함께 분석하면 앱이 어떤 플로우와 동작을 실행할 때 Jank Frame이 발생하는지 추론할 수 있다.
Resource(CPU, 메모리) 상태 로깅
위와 같은 로깅을 통해 서비스에 이상 증상이 있다고 판단할 때, 해당 상황에서 디바이스의 CPU와 메모리 여유 상태를 남긴다.
퍼포먼스 문제가 우리 서비스의 리소스 사용 문제인지, 아니면 디바이스 차원의 퍼포먼스 문제인지를 밝혀내기 위한 로깅이다.
로그 레벨
Android의 Log는 다섯 단계의 레벨을 제공한다.
위쪽일수록 치명적이고, 아래로 갈수록 중요도가 낮아진다.
| 레벨 | 의미 | 언제 쓰는가 | 프로덕션 |
|---|---|---|---|
Log.e() | Error / 에러 | 치명적인 문제가 발생했거나 예외로 기능이 동작하지 않을 때 | 남김 |
Log.w() | Warning / 경고 | 중단할 정도는 아니지만 잠재적인 문제가 될 수 있을 때 | 남김 |
Log.i() | Info / 정보 | 정상적인 흐름과 주요 상태 변화를 기록할 때 | 남김 |
Log.d() | Debug / 디버그 | 개발 중 코드 흐름이나 변수 값을 확인할 때 | 남기지 않음 |
Log.v() | Verbose / 상세 | 개발 중에서도 아주 상세하고 방대한 정보를 기록할 때 | 남기지 않음 |
Log.e() — Error
앱 실행 중 치명적인 문제가 발생했거나, 예외가 잡혀 기능이 정상 동작하지 않을 때 사용한다.
try-catch블록에서 예외가 발생했을 때- API 요청이 완전히 실패하여 화면을 그릴 수 없을 때
- 필수 데이터베이스 트랜잭션이 실패했을 때
try {
val data = parseJson(response)
} catch (e: Exception) {
Log.e("NetworkRepository", "JSON 파싱 실패", e)
}
Log.w() — Warning
앱이 멈추거나 동작을 중단할 정도는 아니지만, 예상치 못한 상황이 발생했거나 잠재적인 문제가 될 수 있을 때 사용한다.
- 권장하지 않는(Deprecated) API를 호출했을 때
- 네트워크 연결이 비정상적이어서 캐시 데이터를 대신 불러올 때
- 복구 가능한 수준의 비정상적인 입력값이 들어왔을 때
if (userToken == null) {
Log.w("AuthManager", "토큰이 없어 게스트 모드로 전환합니다.")
}
Log.i() — Info
앱의 정상적인 흐름과 주요 상태 변화를 기록할 때 사용한다.
운영 환경에서도 흐름을 파악하기 위해 남겨두는 경우가 많다.
- 화면(Activity/Fragment) 진입 및 전환
- 사용자 로그인/로그아웃 성공 시점
- 주요 서비스나 백그라운드 작업의 시작/종료 시점
Log.i("MainActivity", "사용자 로그인 성공: userId = $userId")
Log.d() — Debug
개발 과정에서 코드의 흐름이나 변수 값을 확인하기 위한 용도다.
실제 상용(Release) 앱에서는 출력되지 않도록 관리해야 한다.
- 특정 함수로 전달된 인자(Parameter) 값 확인
- 조건문(
if/when)의 분기 진입 여부 확인 - 뷰(View)의 가시성(Visibility) 상태 변경 확인
Log.d("CartViewModel", "장바구니 아이템 개수: ${itemList.size}")
Log.v() — Verbose
개발 중에서도 아주 상세하고 방대한 정보를 기록할 때 사용한다.
가장 낮은 우선순위를 가지며 Logcat에 엄청난 양의 로그를 남긴다.
- 반복문(Loop) 안에서 매 회차마다 변하는 상태 출력
- 터치 이벤트나 스크롤 이벤트처럼 연속적으로 빠르게 발생하는 이벤트 추적
- 네트워크 통신 시 전송되는 생(Raw) 데이터 전문 출력
Log.v("ScrollListener", "현재 스크롤 Y 좌표: $scrollY")
정리
- 로깅은 개발용 출력이 아니라 QA와 프로덕션에서 문제를 추적하기 위한 흔적이다.
- QA 빌드는 모든 레벨을 남겨도 되지만, 프로덕션은
e,w,i만 남기고 연속적으로 반복되는 로그가 없어야 한다. - 이벤트를 남길 때는 “무엇을 했는지”뿐 아니라 “그때 상태가 어땠는지”를 함께 남겨야 원인을 추론할 수 있다.